""" 
C identical channels process callers in parallel.
Callers arrive according to a Poisson process
Caller service times exponentially distributed
The cell has a finite capacity of size C
Handoff calls have priority over new calls when threshold is met
"""
from SimPy.Simulation import *
from random import expovariate,seed
import time
import logging
import optparse


LOGGING_LEVELS = {'critical': logging.CRITICAL,
                  'error': logging.ERROR,
                  'warning': logging.WARNING,
                  'info': logging.INFO,
                  'debug': logging.DEBUG}
                  
# Model components ------------------------------

class Source(Process):
    """ Source generates callers randomly """
    
    def generate(self, number, meanTBA, resource):
        """ Callers are generated randomly using an exponential
        distribution by calling expovariate(Lambda)
        """
        for i in range(number):
            # Create a new Caller
            c = Caller(name = "Caller%02d"%(i))
            
            # Declare the type of call (new or handoff)
            if (random.randint(0,1) == 0):
                ct = "NEW"
            else:
                ct = "HANDOFF"
            
            # The Caller attempts to make a call
            activate(c, c.make_call(res=resource, calltype=ct))
            # Generate a random time between call arrivals
            lmda = 1.0/meanTBA
            t = expovariate(lmda)
            yield hold, self, t


class Caller(Process):
    """ Caller requests use of a channel and makes a call of random length """
    
    # Class variables (static)
    num_accepted_total = 0
    num_rejected_total = 0
    num_new_total = 0
    num_handoff_total = 0
    num_accepted_new = 0
    num_rejected_new = 0
    num_accepted_handoff = 0
    num_rejected_handoff = 0

    def print_state(self, res):
        """ Displays the number of channels currently in use"""
        logging.debug("%6.2f STATE:    %d channels used" % (now(), (NumInSystem(res))))
        #print("Waiting: %d, Active: %d" % (len(res.waitQ), len(res.activeQ)))
        
    
    def make_call(self, res, calltype="HANDOFF"):
        """ Depending on what type of call comes in, the Caller uses a channel
        in the cell according to the guard channel policy. A handoff call will
        be accepted as long as the number of channels used is less than the 
        capacity of the system. A new call will be accepted as long as the
        number of channels used is less than a THRESHOLD value.
        """
        arrive = now()
        logging.debug("%6.2f %s: ATTEMPTING call - type = %s" % (now(), self.name, calltype))
        # Request a channel in the hotspot

        # Guard channel policy
        if (calltype == "NEW"):
            Caller.num_new_total += 1
            if (NumInSystem(res) < THRESHOLD):
                yield request, self, res
                wait = now() - arrive
                # Make a call of random length
                mu = 1.0/AVG_CALL_TIME
                lengthofcall = expovariate(mu)
                logging.debug("%6.2f %s: ADMIT new call" % (now(), self.name))
                Caller.print_state(self,res)
                # Delay for the length of the call
                yield hold, self, lengthofcall
                # Relinquish use of the channel
                yield release, self, res
                logging.debug("%6.2f %s: COMPLETED new call (length = %.2f)" 
                            % (now(), self.name, lengthofcall))
                Caller.print_state(self,res)
                Caller.num_accepted_total += 1
                Caller.num_accepted_new += 1
            else:
                logging.debug("%6.2f %s: REJECT new call" % (now(), self.name))
                Caller.print_state(self,res)
                Caller.num_rejected_total += 1
                Caller.num_rejected_new += 1
        elif (calltype == "HANDOFF"):
            Caller.num_handoff_total += 1
            if (NumInSystem(res) < MAX_CHANNELS):
                yield request, self, res
                wait = now() - arrive
                # Make a call of random length
                mu = 1.0/AVG_CALL_TIME
                lengthofcall = expovariate(mu)
                logging.debug("%6.2f %s: ADMIT handoff call" % (now(), self.name))
                Caller.print_state(self,res)
                # Delay for the length of the call
                yield hold, self, lengthofcall
                # Relinquish use of the channel
                yield release, self, res
                logging.debug("%6.2f %s: COMPLETED handoff call (length = %.2f)" 
                            % (now(), self.name, lengthofcall))
                Caller.print_state(self,res)
                Caller.num_accepted_total += 1
                Caller.num_accepted_handoff += 1
            else:
                logging.debug("%6.2f %s: REJECT handoff call" % (now(), self.name))
                Caller.print_state(self,res)
                Caller.num_rejected_total += 1
                Caller.num_rejected_handoff += 1
        else:
            logging.debug("invalid call type")


def NumInSystem(R):
    """ Total number of customers using the cell """
    return (len(R.waitQ) + len(R.activeQ))
            

def Print_Welcome():
    """ Display useful information at the beinning of the simulation """
    print "\n", "="*20, "WELCOME", "="*20
    print "MAX_CALLERS = %d, MAX_CHANNELS = %d, THRESHOLD = %d" \
    % (MAX_CALLERS, MAX_CHANNELS, THRESHOLD)
    print ("AVG_CALL_TIME = %1.1f min, AVG_ARRIVAL_TIME = %1.1f min" \
    % (AVG_CALL_TIME, AVG_ARRIVAL_TIME))
    # A diagram showing the regular channels, THRESHOLD, and guard channels
    print "[","="*THRESHOLD,"T","="*(MAX_CHANNELS-THRESHOLD),"]"
    

# Experiment data ------------------------------

MAX_CALLERS = 1100 # Number of calls placed during the simulation
MAX_CHANNELS = 8 # The cell's capacity (in number of channels)
THRESHOLD = int(.75*MAX_CHANNELS) # Threshold is at a percentage of max channels
AVG_CALL_TIME = 3 # Mean call time in minutes (1/Mu)
AVG_ARRIVAL_TIME = 1 # Mean arrival time in minutes (1/Lambda)

MAX_SIM_TIME = 2000.0 # Maximum allowed simulation time in minutes
RAND_SEED = 1234


# Simulation Results ------------------------------

def print_results():
    print "\n", "="*20, "RESULTS", "="*20
    
    accepted_t = (Caller.num_accepted_total, Caller.num_accepted_total*100.0/MAX_CALLERS)
    rejected_t = (Caller.num_rejected_total, Caller.num_rejected_total*100.0/MAX_CALLERS)
    print "Total calls accepted: %d (%.2f%%)" % accepted_t
    print "Total calls rejected: %d (%.2f%%)" % rejected_t
    
    accepted_h = (Caller.num_accepted_handoff, Caller.num_accepted_handoff*100.0/Caller.num_handoff_total)
    rejected_h = (Caller.num_rejected_handoff, Caller.num_rejected_handoff*100.0/Caller.num_handoff_total)
    print "Handoff calls accepted: %d (%.2f%%)" % accepted_h
    print "Handoff calls rejected: %d (%.2f%%)" % rejected_h
    
    accepted_n = (Caller.num_accepted_new, Caller.num_accepted_new*100.0/Caller.num_new_total)
    rejected_n = (Caller.num_rejected_new, Caller.num_rejected_new*100.0/Caller.num_new_total)
    print "New calls accepted: %d (%.2f%%)" % accepted_n
    print "New calls rejected: %d (%.2f%%)" % rejected_n
    
        
# Model/Experiment ------------------------------

def main():
    # Parse command line argument(s) for log level and/or log file
    parser = optparse.OptionParser()
    parser.add_option('-l', '--logging-level', help='Logging level')
    parser.add_option('-f', '--logging-file', help='Logging file name')
    (options, args) = parser.parse_args()
    logging_level = LOGGING_LEVELS.get(options.logging_level, logging.NOTSET)
    logging.basicConfig(level=logging_level, filename=options.logging_file,
                      format='%(message)s',
                      datefmt='%Y-%m-%d %H:%M:%S')
    
    seed(RAND_SEED)
    # One hotspot with fixed number of channels
    r = Resource(capacity=MAX_CHANNELS, name="Hotspot", unitName="Channel", monitored=True)
    Print_Welcome()

    initialize()
    # The source will generate the callers randomly
    s = Source('Source')
    # Kick off the source to create the callers and run the simulation
    activate(s, s.generate(number=MAX_CALLERS, meanTBA=AVG_ARRIVAL_TIME, resource=r), at=0.0)
    simulate(until=MAX_SIM_TIME)
    time.sleep(1)
    print_results()
    	
if __name__ == '__main__':
  main()
